> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/jaypopat/cf_ai_duet/llms.txt
> Use this file to discover all available pages before exploring further.

# Sandboxes

> Isolated command execution with Cloudflare Containers

## Overview

Duet uses Cloudflare Sandboxes (container-based Durable Objects) to provide isolated, secure command execution environments. Each room gets its own dedicated sandbox instance that persists for the lifetime of the room.

## Sandbox Architecture

Sandboxes are implemented as container-based Durable Objects:

```toml theme={null}
[durable_objects]
bindings = [
  { name = "DUET_AGENT", class_name = "DuetAgent" },
  { name = "Sandbox", class_name = "Sandbox" },
]

[[containers]]
class_name = "Sandbox"
image = "./Dockerfile"

[[migrations]]
tag = "v2"
new_sqlite_classes = ["Sandbox"]
```

**Source:** `~/workspace/source/cf-worker/wrangler.toml:6-21`

Each sandbox:

* Runs in an isolated container environment
* Has its own filesystem state
* Is bound to a specific room via naming convention
* Persists across multiple command executions

## Sandbox Naming

Sandboxes are named using the pattern `sandbox-{roomId}`:

```typescript theme={null}
const sandbox = getSandbox(this.env.Sandbox, `sandbox-${roomId}`);
```

**Source:** `~/workspace/source/cf-worker/index.ts:196`

This ensures:

* Each room has its own isolated sandbox
* Multiple users in the same room share the same sandbox
* Different rooms cannot access each other's sandboxes

## Command Execution

### Direct Execution Endpoint

The `/sandbox/exec` endpoint allows direct command execution:

```typescript theme={null}
private async handleSandboxExec(roomId: string, rawBody: unknown): Promise<Response> {
  const parseResult = SandboxExecRequestSchema.safeParse(rawBody);

  if (!parseResult.success) {
    return Response.json(
      {
        error: "invalid request",
        details: z.flattenError(parseResult.error).fieldErrors,
      },
      { status: 400 }
    );
  }

  const data = parseResult.data;
  const sandboxName = `sandbox-${roomId}`;

  try {
    const sandbox = getSandbox(this.env.Sandbox, sandboxName);
    const result = await sandbox.exec(data.cmd);

    return Response.json({ result, sandboxName });
  } catch (error) {
    return Response.json(
      {
        error: `sandbox execution failed: ${error instanceof Error ? error.message : "unknown error"}`,
      },
      { status: 500 }
    );
  }
}
```

**Source:** `~/workspace/source/cf-worker/index.ts:210-242`

### Request Validation

Commands are validated using Zod schema:

```typescript theme={null}
const SandboxExecRequestSchema = z.object({
  cmd: z.string().min(1, "Command cannot be empty"),
});
```

**Source:** `~/workspace/source/cf-worker/index.ts:14-16`

### Execution Result

Sandbox execution returns stdout and stderr:

```typescript theme={null}
const { stderr, stdout } = await sandbox.exec(cmd);
```

The result structure:

```typescript theme={null}
type ExecResult struct {
  Stdout string `json:"stdout"`
  Stderr string `json:"stderr"`
}

type ExecResponse struct {
  Result      ExecResult `json:"result"`
  SandboxName string     `json:"sandboxName"`
  Error       string     `json:"error,omitempty"`
}
```

**Source:** `~/workspace/source/internal/ai/client.go:54-65`

## AI-Triggered Execution

The AI can trigger sandbox commands automatically using `<run>` tags:

```typescript theme={null}
private async executeCommands(text: string, roomId: string): Promise<string> {
  const matches = Array.from(text.matchAll(/<run>([\s\S]*?)<\/run>/g));
  let result = text;

  for (const match of matches) {
    const cmd = match[1]?.trim();
    if (!cmd) {
      continue;
    }

    try {
      const sandbox = getSandbox(this.env.Sandbox, `sandbox-${roomId}`);
      const { stderr, stdout } = await sandbox.exec(cmd);

      const summary =
        stdout.slice(0, 500) || stderr.slice(0, 500) || "[no output]";
      result += `\n\nOutput (${cmd}):\n${summary}`;
    } catch (e) {
      const msg = e instanceof Error ? e.message : String(e);
      result += `\n\nError (${cmd}):\n${msg}`;
    }
  }
  return result.replace(/<run>[\s\S]*?<\/run>/g, "").trim() || "";
}
```

**Source:** `~/workspace/source/cf-worker/index.ts:185-208`

The execution flow:

1. **Extract commands** - Regex finds all `<run>...</run>` tags
2. **Execute each command** - Run in the room's sandbox
3. **Capture output** - Get first 500 chars of stdout/stderr
4. **Append to response** - Add output after AI's explanation
5. **Remove tags** - Strip `<run>` tags from final response

## Output Truncation

To prevent response bloat, output is limited:

```typescript theme={null}
const summary = stdout.slice(0, 500) || stderr.slice(0, 500) || "[no output]";
```

**Source:** `~/workspace/source/cf-worker/index.ts:199-200`

This ensures:

* Responses remain reasonably sized
* Users see immediate feedback
* Long outputs don't overwhelm the UI

## Error Handling

Execution errors are caught and included in the response:

```typescript theme={null}
try {
  const sandbox = getSandbox(this.env.Sandbox, `sandbox-${roomId}`);
  const { stderr, stdout } = await sandbox.exec(cmd);
  // ...
} catch (e) {
  const msg = e instanceof Error ? e.message : String(e);
  result += `\n\nError (${cmd}):\n${msg}`;
}
```

**Source:** `~/workspace/source/cf-worker/index.ts:195-205`

Errors are displayed to the user rather than failing silently.

## Sandbox Lifecycle

### Creation

Sandboxes are created on-demand when first accessed:

```typescript theme={null}
const sandbox = getSandbox(this.env.Sandbox, `sandbox-${roomId}`);
```

The `getSandbox` function from `@cloudflare/sandbox` handles lazy initialization.

### Persistence

Sandboxes persist across multiple command executions, maintaining:

* Filesystem state
* Working directory
* Installed packages or files

This allows multi-step workflows like:

1. Create a file
2. Modify it
3. Run it

### Cleanup

When a room is deleted, its sandbox is destroyed:

```typescript theme={null}
private async handleCleanup(roomId: string): Promise<Response> {
  const errors: string[] = [];

  // Reset agent state
  this.setState({ messages: [] });

  // Terminate sandbox
  try {
    const sandbox = getSandbox(this.env.Sandbox, `sandbox-${roomId}`);
    await sandbox.destroy();
  } catch (e) {
    errors.push(`sandbox: ${e instanceof Error ? e.message : String(e)}`);
  }

  if (errors.length > 0) {
    return Response.json({ cleaned: true, errors }, { status: 207 });
  }

  return Response.json({ cleaned: true, roomId });
}
```

**Source:** `~/workspace/source/cf-worker/index.ts:244-266`

Cleanup errors are collected and returned with a 207 Multi-Status response.

## Client API

The Go client provides a method to execute commands:

```go theme={null}
func (c *Client) ExecCommand(ctx context.Context, roomID, cmd string) (*ExecResponse, error) {
  url := fmt.Sprintf("%s/api/rooms/%s/sandbox/exec", c.baseURL, roomID)

  body := ExecRequest{
    Cmd: cmd,
  }

  jsonBody, err := json.Marshal(body)
  if err != nil {
    return nil, fmt.Errorf("marshal request: %w", err)
  }

  req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(jsonBody))
  if err != nil {
    return nil, fmt.Errorf("create request: %w", err)
  }
  req.Header.Set("Content-Type", "application/json")

  resp, err := c.http.Do(req)
  if err != nil {
    return nil, fmt.Errorf("send request: %w", err)
  }
  defer resp.Body.Close()

  var result ExecResponse
  if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
    return nil, fmt.Errorf("decode response: %w", err)
  }

  if result.Error != "" {
    return nil, fmt.Errorf("sandbox error: %s", result.Error)
  }

  return &result, nil
}
```

**Source:** `~/workspace/source/internal/ai/client.go:128-162`

## Example Usage

### Direct Command Execution

```bash theme={null}
POST /api/rooms/room-123/sandbox/exec
Content-Type: application/json

{
  "cmd": "echo 'Hello from sandbox'"
}
```

Response:

```json theme={null}
{
  "result": {
    "stdout": "Hello from sandbox\n",
    "stderr": ""
  },
  "sandboxName": "sandbox-room-123"
}
```

### AI-Triggered Execution

User: "Create a hello.txt file with 'Hello World'"

AI Response:

```
I'll create the file for you.
<run>echo 'Hello World' > hello.txt</run>
```

Final Response to User:

```
I'll create the file for you.

Output (echo 'Hello World' > hello.txt):
[no output]
```

## Next Steps

* [LLM Integration](/architecture/llm-integration) - Learn how AI triggers sandbox commands
* [Durable Objects](/architecture/durable-objects) - Understand room state management
* [Cloudflare Workers](/architecture/cloudflare-workers) - See the routing layer
